﻿using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;

namespace System.Web.Mvc
{
    internal class ActionRouteDescriptor
    {
        private ReadOnlyCollection<Func<IReversibleModelBinder>> binderFactories;

        public MethodInfo ActionInfo { get; private set; }

        public ActionRouteDescriptor(MethodInfo actionInfo)
        {
            if (actionInfo == null)
            {
                throw new ArgumentNullException("actionInfo");
            }

            this.ActionInfo = actionInfo;
            this.Name = GetName(actionInfo);
            this.RouteName = GetRouteName(actionInfo);
            this.ControllerName = GetControllerName(actionInfo.ReflectedType);
            this.binderFactories = new ReadOnlyCollection<Func<IReversibleModelBinder>>(actionInfo.GetParameters().Select(p => GetBinderFactory(p)).ToList());
        }

        public string Name { get; private set; }

        public string RouteName { get; private set; }

        public string ControllerName { get; private set; }

        public IReversibleModelBinder GetBinder(int index)
        {
            return this.binderFactories[index].Invoke();
        }

        private static Func<IReversibleModelBinder> GetBinderFactory(ParameterInfo parameterInfo)
        {
            CustomModelBinderAttribute binderAttribute = GetBinderAttribute(parameterInfo) ?? GetBinderAttribute(parameterInfo.ParameterType);

            IModelBinder binder = binderAttribute != null ? binderAttribute.GetBinder() : ModelBinders.Binders.GetBinder(parameterInfo.ParameterType);
            if (binder is IReversibleModelBinder)
            {
                if (IsReusable(binder.GetType()))
                {
                    return () => binder as IReversibleModelBinder;
                }

                if (binderAttribute != null && IsReusable(binderAttribute.GetType()))
                {
                    return () => binderAttribute.GetBinder() as IReversibleModelBinder;
                }

                return () =>
                {
                    CustomModelBinderAttribute innerAttr = GetBinderAttribute(parameterInfo) ?? GetBinderAttribute(parameterInfo.ParameterType);
                    return innerAttr != null ? innerAttr.GetBinder() as IReversibleModelBinder : ModelBinders.Binders.GetBinder(parameterInfo.ParameterType) as IReversibleModelBinder;
                };
            }
            return () => null;
        }

        private static bool IsReusable(ICustomAttributeProvider provider)
        {
            return provider.GetCustomAttributes(typeof(ReusableAttribute), true).Length > 0;
        }

        private static string GetControllerName(Type controllerType)
        {
            string controllerTypeName = controllerType.Name;
            if (!controllerTypeName.EndsWith("Controller", StringComparison.OrdinalIgnoreCase))
            {
                throw new ArgumentException("The controller name must end with 'Controller'.", "action");
            }

            string controllerName = controllerTypeName.Substring(0, controllerTypeName.Length - "Controller".Length);
            if (controllerName.Length == 0)
            {
                throw new ArgumentException("Cannot route to the Controller class", "action");
            }

            return controllerName;
        }

        private static string GetName(MethodInfo actionInfo)
        {
            ActionNameAttribute attribute = (ActionNameAttribute)actionInfo.GetCustomAttributes(typeof(ActionNameAttribute), false).SingleOrDefault();
            return attribute == null ? actionInfo.Name : attribute.Name;
        }

        private static string GetRouteName(ICustomAttributeProvider actionInfo)
        {
            RouteNameAttribute attribute = (RouteNameAttribute)actionInfo.GetCustomAttributes(typeof(RouteNameAttribute), false).SingleOrDefault();
            return attribute == null ? String.Empty : attribute.Name;
        }

        private static CustomModelBinderAttribute GetBinderAttribute(ICustomAttributeProvider element)
        {
            CustomModelBinderAttribute[] attrs = (CustomModelBinderAttribute[])element.GetCustomAttributes(typeof(CustomModelBinderAttribute), true /* inherit */);
            if (attrs != null)
            {
                switch (attrs.Length)
                {
                    case 0: return null;
                    case 1: return attrs[0];
                    default:
                        throw new InvalidOperationException("Multiple binder is forbidden.");
                }
            }
            return null;
        }
    }
}
